package org.nkumar.mesex;

import net.sourceforge.pmd.AbstractJavaRule;
import net.sourceforge.pmd.ast.ASTCompilationUnit;
import net.sourceforge.pmd.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.ast.ASTName;
import net.sourceforge.pmd.ast.Node;
import net.sourceforge.pmd.ast.ASTPrimaryExpression;
import net.sourceforge.pmd.ast.ASTExpression;
import net.sourceforge.pmd.ast.ASTLiteral;
import net.sourceforge.pmd.ast.SimpleNode;
import net.sourceforge.pmd.ast.SimpleJavaNode;
import net.sourceforge.pmd.ast.Comment;

import org.jaxen.JaxenException;

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public final class MesExRule extends AbstractJavaRule
{
    private final List<MesExRuleMetadata> metadatas;
    private final Map<String, MesExRuleMetadata> variables = new HashMap<String, MesExRuleMetadata>();
    private final Map<String, Pattern> variableUsagePattern = new HashMap<String, Pattern>();
    private final Map<String, String> messages;

    public MesExRule(Map<String, String> messages, MesExRuleMetadata... metadatas)
    {
        if (messages == null)
        {
            throw new IllegalArgumentException("Message container was null");
        }
        this.messages = messages;
        if (metadatas == null || metadatas.length == 0)
        {
            throw new IllegalArgumentException("Empty metadata list passed");
        }
        this.metadatas = Collections.unmodifiableList(new ArrayList<MesExRuleMetadata>(Arrays.asList(metadatas)));
    }

    @Override
    public Object visit(final ASTCompilationUnit node, final Object data)
    {
        variables.clear();
        variableUsagePattern.clear();
        return super.visit(node, data);
    }

    private MesExRuleMetadata matchMetadataType(final String type)
    {
        for (MesExRuleMetadata metadata : metadatas)
        {
            if (metadata.getClassName().equals(type) || metadata.getFullClassName().equals(type))
            {
                return metadata;
            }
        }
        return null;
    }

    @Override
    public final Object visit(final ASTVariableDeclaratorId node, final Object data)
    {
        if (node.getImage() != null)
        {
            final String type = node.getNameDeclaration().getTypeImage();
            if (type != null)
            {
                MesExRuleMetadata metadata = matchMetadataType(type);
                if (metadata != null)
                {
                    String variableName = node.getImage();
                    variables.put(variableName, metadata);
                    variableUsagePattern.put(variableName,
                            Pattern.compile(variableName + "[.]" + metadata.getMethodCallPattern()));
                }
            }
        }
        return super.visit(node, data);
    }

    @Override
    public final Object visit(final ASTName node, final Object data)
    {
        if (!isEmpty(node.getImage()))
        {
            KeyValue keyValue = extractMessage(node);
            //if a key value is found, keep accumulating the same
            if (keyValue != null)
            {
                messages.put(keyValue.getKey(), keyValue.getValue());
            }
        }
        return super.visit(node, data);
    }

    private KeyValue extractMessage(final ASTName node)
    {
        assert node.getImage() != null;
        //check for each variable
        for (String variableName : variableUsagePattern.keySet())
        {
            //match the method pattern
            final Pattern callPattern = variableUsagePattern.get(variableName);
            final Matcher matcher = callPattern.matcher(node.getImage());
            if (!matcher.matches())
            {
                continue;
            }
            //if the variable name and any of the supported methods matched then try to extract the message
            return matchMesage(node, matcher.group(1), variables.get(variableName));
        }
        return null;
    }

    private static final String ARGUMENTS_EXPR_XPATH = "PrimarySuffix/Arguments/ArgumentList/Expression";


    private static KeyValue matchMesage(ASTName node, final String methodName, final MesExRuleMetadata metadata)
    {
        //get the root node for this method
        final Node parent = node.getNthParent(2);
        if (!(parent instanceof ASTPrimaryExpression))
        {
            return null;
        }
        final ASTPrimaryExpression parentExpr = (ASTPrimaryExpression) parent;

        //navigate to the arguments
        final List children;
        try
        {
            children = parentExpr.findChildNodesWithXPath(ARGUMENTS_EXPR_XPATH);
        }
        catch (JaxenException e)
        {
            return null;
        }
        //check if both the key index and message index is within the number of arguments passed
        final int keyParamIndex = metadata.getKeyParamIndexForMethod(methodName);
        if (keyParamIndex == -1 || children.size() < keyParamIndex + 1)
        {
            return null;
        }
        //extract the key string value
        return StringLiteralUtil.extractStringLiteral((Node) children.get(keyParamIndex));
    }

    private static final class KeyValue
    {
        private final String key;
        private final String value;

        public KeyValue(String key, String value)
        {
            this.key = key;
            this.value = value;
        }

        public String getKey()
        {
            return key;
        }

        public String getValue()
        {
            return value;
        }
    }

    /**
     * Returns true if the passed string is null or of length 0.
     * @param str
     * @return true if the passed string is null or of length 0
     */
    static boolean isEmpty(final String str)
    {
        return str == null || str.length() == 0;
    }

    private static final class StringLiteralUtil
    {
        private static final String SIMPLE_LITERAL_EXPR_XPATH = "PrimaryExpression/PrimaryPrefix/Literal";
        private static final Pattern STRING_LITERAL_PATTERN = Pattern.compile("^\"(.*)\"$", Pattern.DOTALL);

        private StringLiteralUtil()
        {
        }

        /**
         * Returns the extract string literal from the node.
         * It first tries to extract simple string literal
         * and if that fails tries to find if there is any
         * concatenation of string literals happening.
         * This method may return null if no string
         * literal could be extracted out.
         * @param node
         * @return extracted (if any) string literal from a node
         */
        static KeyValue extractStringLiteral(final Node node)
        {
            if (!(node instanceof ASTExpression))
            {
                return null;
            }
            final ASTExpression expr = (ASTExpression) node;
            return getSimpleLiteral(expr);
        }

        /**
         * For cases like "key".
         * <pre>
         * Expression
         *   PrimaryExpression
         *     PrimaryPrefix
         *       Literal:"key"
         * </pre>
         * @param expr
         * @return the string literal for cases like above
         */
        private static KeyValue getSimpleLiteral(final ASTExpression expr)
        {
            final List children;
            try
            {
                children = expr.findChildNodesWithXPath(SIMPLE_LITERAL_EXPR_XPATH);
            }
            catch (Exception ignore)
            {
                return null;
            }
            if (children.size() == 1)
            {
                return getStringLiteral((Node) children.get(0));
            }
            return null;
        }

        /**
         * Gets the string from the node and removes the double quotes surrounding the same.
         * @param node
         * @return extracted string literal from the node
         */
        private static KeyValue getStringLiteral(final Node node)
        {
            if (node instanceof ASTLiteral)
            {
                final String literal = ((SimpleNode) node).getImage();
                if (literal != null)
                {
                    final Matcher matcher = STRING_LITERAL_PATTERN.matcher(literal);
                    if (matcher.matches())
                    {
                        final List<Comment> comments = ((SimpleJavaNode) node).getComments();
                        if (!comments.isEmpty())
                        {
                            final String commentString = uncomment(comments.get(0).getImage());
                            return new KeyValue(matcher.group(1), commentString);
                        }
                    }
                }
            }
            return null;
        }

        private static String uncomment(String comment)
        {
            if (comment == null || comment.length() < 3)
            {
                return null;
            }
            if (comment.startsWith("//"))
            {
                return comment.substring(2).trim();
            }
            Pattern pattern = Pattern.compile("/[*]*(.*)[*]/", Pattern.DOTALL);
            Matcher matcher = pattern.matcher(comment);
            if (matcher.matches())
            {
                return matcher.group(1).trim();
            }
            return null;
        }

    }

}
